package org.mentalsmash.crossroads.parsing;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Stack;

import org.apache.log4j.Logger;

/**
 * CrossroadsTokenizer is an application-specific scanner used to separate tokens.
 * Is is especially built to be used with the prolog parser.
 * It uses whitespace and a bunch of special characters to distinguish different tokens.
 * Moreover this class manages indentation in the source file. It uses a stack to remember last indentation level and substitutes indentations
 * and dedentation with appropriate tokens. NOTE that this class doesn't manage well mixed indentation char, so one should use only spaces or
 * only tabs, but never a mix of the two.
 * @author Andrea Reale
 *
 */
public class CrossroadsScanner{

	private static Logger log = Logger.getLogger(CrossroadsScanner.class);
	protected static final String NEWLINE_TOKEN = "<NL>";
	protected static final String INDENT_TOKEN = "<INDENT>";
	protected static final String DEDENT_TOKEN = "<DEDENT>";

	private char _cursor;
	
	/*This couple of vars are to keep track of  the position of the reader*/
	private int _curRow;
	private int _curCol;
	
	private boolean _lineStart = true;
	private boolean _initialized = false;
	private boolean _eof = false;
	
	/*This var is used to keep track of the fact the scanner is searching for a closing double quote*/
	private boolean _inDQuotesState = false;
	

	//This variable is set when an exception is found and it is used to return at the next call for next
	private Exception placeHolder = null;

	private Stack<Integer> _indentingLevel;
	private BufferedReader _reader;

	private StringBuilder _tokenContainer;
	private Queue<String> _tokensQueue;

	/**
	 * Create a new CrossordScanner for tokenizing a string
	 * @param source the string to be scanned
	 */
	public CrossroadsScanner(String source){
		setReader(new BufferedReader(new StringReader(source)));
		setIndentingLevel(new Stack<Integer>());
		setTokenContainer(new StringBuilder());
		setTokensQueue(new LinkedList<String>());
	}
	/**
	 * Create a new Crossword scanner for tokenizing an InputStream
	 * @param source the inputstream to tokenize
	 */
	public CrossroadsScanner(InputStream source){
		setReader(new BufferedReader(new InputStreamReader(source)));
		setIndentingLevel(new Stack<Integer>());
		setTokenContainer(new StringBuilder());
		setTokensQueue(new LinkedList<String>());
	}
	/**
	 * Create a crossoroads scanner that will tokenize the contents of a file
	 * @param source an instance of File pointing to to text file to be scanned
	 * @throws FileNotFoundException if the File referenced by the passed arg is not found
	 */
	public CrossroadsScanner(File source) throws FileNotFoundException{
		setReader(new BufferedReader(new FileReader(source)));
		setIndentingLevel(new Stack<Integer>());
		setTokenContainer(new StringBuilder());
		setTokensQueue(new LinkedList<String>());
	}
	


	private void init() throws ParsingException, IOException{
		if (consumeWhiteSpaces() != 0)
			throw new ParsingException("No first level indentaion allowed");
		getIndentingLevel().push(0);
		getTokenContainer().append("");
		setCurCol(1);
		setCurRow(1);
		
		setInitialized(true);

	}


	/**
	 * This method should be called while the cursor for the source that is being scanned is at the start
	 * of a new line. It manages to handle indentations and returns an array containing the indentation tokens to be returned before
	 * continuing to Scan the rest of the line
	 * @return An array of Strings representing the indentation tokens that should be returned before scanning the rest of the line
	 * @throws ParsingException If indentation is wrong
	 * @throws IOException if read() fails for other matters regarding the cursor.
	 */
	private String[] newline() throws ParsingException, IOException{
		setLineStart(true);
		_curRow++;
		_curCol = 1;
		
		int whitespaces = consumeWhiteSpaces();

		ArrayList<String> indentationTokens = manageIndentation(whitespaces);

		if(indentationTokens.size() == 0) return null;
		else return indentationTokens.toArray(new String[indentationTokens.size()]);
	}
	
	private ArrayList<String> manageIndentation(int whitespaces) throws SyntacticException {
		int curWhitespace = getIndentingLevel().pop();

		ArrayList<String> indentationTokens = new ArrayList<String>();

		if(whitespaces == curWhitespace)
			getIndentingLevel().push(curWhitespace);
		if(whitespaces > curWhitespace){
			getIndentingLevel().push(curWhitespace);
			getIndentingLevel().push(whitespaces);
			indentationTokens.add(INDENT_TOKEN);
		} else if (whitespaces < curWhitespace) {
			while(whitespaces != curWhitespace){
				indentationTokens.add(DEDENT_TOKEN);
				curWhitespace = getIndentingLevel().pop();
				if(whitespaces > curWhitespace)
					throw new SyntacticException(getCurRow(),getCurCol(),
							"Wrong indentation at (" + getCurRow() + "," + getCurCol() + ") .");
			}
			getIndentingLevel().push(curWhitespace);
		}
		return indentationTokens;
	}



	/**
	 * Consume whitespace and returns the number of withespace consumed.
	 * At the end of this call the var _cursor contains a non-whitespace char
	 * @return the number of whitespaces
	 * @throws IOException
	 */
	private int consumeWhiteSpaces() throws IOException{

		int acc = 0;
		if(isWhiteSpace(getCursor())) acc++;
		int ch;
		while((ch=getReader().read())!=-1 && isWhiteSpace((char)ch)){
			acc++;
		}


		//If we reached EOF
		if(ch == -1)
			_eof = true;
		else {
			setCursor((char)ch);
			setCurCol(getCurCol()+acc);
		}

		return acc;
	}

	private boolean isWhiteSpace(char c){
		return (c == ' ' || c == '\t');
	}




	/**
	 * True if the scanner can return a next token. The fact the this method returns yes does not necessarly mean
	 * that a subsequent call to next() will not throw an Exception! It just means that the scanner can try to read another token
	 * @return true if the scanner can read a next token
	 */
	public boolean hasNext()  {
		
		if(getTokensQueue().size() > 0)
			return true;
		else if(getTokensQueue().size() == 0){
			if(_eof)
				return false;
			
			try {
				scanNextAndEnqueue();
			} catch (ParsingException e) {
				log.warn("Parsing exception", e);
				placeHolder = e;
				//TODO: PLEASE FIX THIS
				return true; //this is a ignoble hack to return exception at the next call of next()
				//e.printStackTrace();
			} catch (IOException e) {
				log.warn("IO exception while parsing", e);
				placeHolder = e;
				//TODO: PLEASE FIX THIS
				return true; //this is a ignoble hack to return exception at the next call of next()
				//e.printStackTrace();
			}
			if(getTokensQueue().size() == 0)
				return false;

			return true;
		}

		return false;
	}


	/**
	 * Returns the next "token" from the source being scanned.
	 * @return the next token
	 * @throws ParsingException if any scanner error is found.
	 */
	public String next() throws ParsingException{
		if(placeHolder != null){
			Exception toThrow = placeHolder;
			placeHolder = null;
			if (toThrow instanceof ParsingException)
				throw (ParsingException) toThrow;
			else if (toThrow instanceof IOException)
				throw (ParsingException) toThrow;
			else
				throw new RuntimeException("Unexpected type of exception", toThrow);
		}

		if (getTokensQueue().size() == 0)
			throw new NoSuchElementException();
		return getTokensQueue().poll();

	}

	/**
	 * Scans for the next token and adds it to the token queue
	 * @throws ParsingException If some kind of parsing error occurs
	 * @throws IOException if some exception related to the source occurs
	 */
	private void scanNextAndEnqueue() throws ParsingException, IOException {


		if(!isInitialized()){
			try {
				init();
			} catch (Exception e) {
				log.fatal("Error initializing file to scan",e);
				throw new NoSuchElementException("Initialization error.");
			}
		}

		/* Do we have a token to return?*/
		boolean tokenBuilt =false;

		/* 
		 * Loop until we do not find an EOF or sombody else has reached EOF (and has set the _eof switch)
		 * or until a new token to return is not built
		 */
		while((int)getCursor() != -1 && !_eof && !tokenBuilt){
			
			switch (_cursor) {
			//If it is a blank return the token being construced
			case ' ':
			case '\t':
				consumeWhiteSpaces();

				//Since I found a space it's time to return the token beeing built
				tokenBuilt = tryOfferToken();

				break;
			case '\n':
				//Since I found a newline charcater it's time to return the token beeing built
				tryOfferToken();
				
				//Adding a <NL> token if the token is not the first of the line
				if(!isLineStart())
					getTokensQueue().offer(NEWLINE_TOKEN);
				else
					log.debug("newline at the start of a line");
				String[] indentationTokens = newline();
				if(indentationTokens != null)
					for(String tk : indentationTokens) getTokensQueue().add(tk);
				tokenBuilt = true;
				break;
			//Quoted text: everything inside a couple of double quotes is to be returned as a single token
			case '"':
				//I'm marking that I am no more at the start of a line
				if(isLineStart())
					setLineStart(false);
				
				tokenBuilt = manageDQuotedToken();
				
				setCursor((char)getReader().read());
				_curCol++;
				break;
			case ':':
				//Marking that i'm no more at the start of a line
				if(isLineStart())
					setLineStart(false);
				//The colon is a token itself so before returning it it's better to return what's currently being built
				tryOfferToken();
				getTokensQueue().offer(":");
				tokenBuilt = true;
				setCursor((char)getReader().read());
				_curCol++;
				break;
			default:
				//Any other 'non special' character: accumulate it in the builder;
				//I'm marking that I am no more at the start of a line
				if(isLineStart())
					setLineStart(false);
				getTokenContainer().append(getCursor());
				setCursor((char)getReader().read());
				_curCol++;
				break;

			} //end of the switch statement
		} //end of the loop

		//If I reached the eof, offer what's left and set the flag
		if((int)getCursor() == -1){
			_eof = true;
			tryOfferToken();
		}
	}
	
	private boolean manageDQuotedToken() throws SyntacticException {
		boolean tokenBuilt;
		if(!isInDQuotesState()){
			//there is a token beeing built but I found a double quote. This is a wrong position
			if(getTokenContainer().length() > 0){
				throw new SyntacticException(getCurRow(),getCurCol(),
						"Double quotes are not allowed inside a token, at (" + getCurRow() + "," + getCurCol() + ") .");
			}
			//start a double quote sequence
			setInDQuotesState(true);
		}
		else { // I'm in a double quote sequence so those quotes mark the end of the sequence
			setInDQuotesState(false);
			tryOfferToken();
		}
		getTokensQueue().offer("\"");
		tokenBuilt = true;
		return tokenBuilt;
	}
	
	private boolean tryOfferToken() {
		boolean tokenBuilt = false;
		
		if(getTokenContainer().length() > 0){
			getTokensQueue().offer(getTokenContainer().toString());
			setTokenContainer(new StringBuilder());
			tokenBuilt = true;
		}
		return tokenBuilt;
	}
	
	
	/* ********************************************************* */
	/*             GETTERS/SETTERS                               */
	/*                                                           */
	/* ********************************************************* */
	private char getCursor() {
		return _cursor;
	}
	private void setCursor(char currentChar) {
		_cursor = currentChar;
	}
	private int getCurRow() {
		return _curRow;
	}
	private void setCurRow(int curRow) {
		_curRow = curRow;
	}
	private int getCurCol() {
		return _curCol;
	}
	private void setCurCol(int curCol) {
		_curCol = curCol;
	}
	private boolean isLineStart() {
		return _lineStart;
	}
	private void setLineStart(boolean lineStart) {
		_lineStart = lineStart;
	}
	private boolean isInitialized() {
		return _initialized;
	}
	private void setInitialized(boolean initialized) {
		_initialized = initialized;
	}
	private Stack<Integer> getIndentingLevel() {
		return _indentingLevel;
	}
	private void setIndentingLevel(Stack<Integer> indentingLevel) {
		_indentingLevel = indentingLevel;
	}
	private BufferedReader getReader() {
		return _reader;
	}
	private void setReader(BufferedReader reader) {
		_reader = reader;
	}
	private StringBuilder getTokenContainer() {
		return _tokenContainer;
	}
	private void setTokenContainer(StringBuilder tokenContainer) {
		_tokenContainer = tokenContainer;
	}
	private Queue<String> getTokensQueue() {
		return _tokensQueue;
	}
	private void setTokensQueue(Queue<String> tokensQueue) {
		_tokensQueue = tokensQueue;
	}
	private void setInDQuotesState(boolean inDQuotesState) {
		_inDQuotesState = inDQuotesState;
	}
	private boolean isInDQuotesState() {
		return _inDQuotesState;
	}



	
	



}
